package com.superdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.superdp.utils.RedisConstants.*;

/**
 * ClassName: CacheClient
 * Package: com.superdp.utils
 * Description:
 *
 * @Author: XKing
 * @Create: 2023/7/2 - 15:15
 * @Version: 1.0
 */
@Component
@Slf4j
public  class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }


    /**
     * 缓存重建线程池
     */
    private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);

    public <R,ID>R queryWithLogicExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallback,
                                        Long time,TimeUnit unit){
        //获取key
        String key=keyPrefix+id;
        //根据key查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        //命中缓存
        if(json!=null){
            //查看是否为空字符串
            if(StrUtil.isBlank(json)){
                //是的话直接返回null
                return null;
            }
            //不为空字符串判断逻辑过期时间是否过期了
            RedisData redisData = JSONUtil.toBean(json, RedisData.class);
            //Data转成Shop
            R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
            LocalDateTime expireTime = redisData.getExpireTime();
            //逻辑过期时间在当前时间后边，未过期，直接返回
            if(expireTime.isAfter(LocalDateTime.now())){
                return r;
            }
            //过期了，尝试获取锁
            String lockKey=LOCK_SHOP_KEY+id;
            try {
                boolean b = tryLock(lockKey);
                if(!b){
                    //获取锁失败，返回过期的数据
                    return r;
                }
                //获取锁成功，重新开启一个线程去重建缓存
                CACHE_REBUILD_EXECUTOR.submit(()->{
                    //缓存重建
                    //1.查询数据库最新的信息
                    R r1 = dbFallback.apply(id);
                    //2.写入缓存
                    this.setWithLogicExpire(key,r1,time,unit);
//                    try {
//                        //模拟缓存重建的时间
//                        Thread.sleep(200);
//                    } catch (InterruptedException e) {
//                        throw new RuntimeException(e);
//                    }
                });
                return r;
            }catch (Exception e){
                throw new RuntimeException(e);
            }finally {
                unLock();
            }
        }
        //未命中缓存直接返回null
        return null;
    }
    /*
    获取锁，使用redis中String数据结构的setnx命令
    */
    private boolean tryLock(String lockKey) {
        Boolean b = stringRedisTemplate.opsForValue()
                .setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(b);
    }
    /**
     * 释放锁
     */
    private void unLock(){
        stringRedisTemplate.delete(LOCK_SHOP_KEY);
    }

    /**
     * 解决缓存穿透工具类，可以应对各种业务的缓存穿透问题
     * @param prefix
     * @param id
     * @param type
     * @param dbFallback
     * @param time
     * @param unit
     * @return
     * @param <R>
     * @param <ID>
     */
    public <R,ID> R queryWithPassThrough(String prefix, ID id, Class<R> type, Function<ID,R> dbFallback,
                                         Long time,TimeUnit unit){
        //获取key
        String key=prefix+id;
        //根据key查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        //命中缓存
        if(json!=null){
            //查看是否为空字符串
            if(StrUtil.isBlank(json)){
                //是的话直接返回null
                return null;
            }
            //不为空字符串则正常返回shop
            R r = JSONUtil.toBean(json,type);
            return r;
        }
        //未命中缓存查询数据库
        R r = dbFallback.apply(id);
        //数据也没有直接返回null
        if(r==null){
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }
        //缓存重建
        this.set(key,r,time,unit);
        return r;
    }
    /**
     * 将任意对象存入redis并设置TTL
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void set(String key,Object value,Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }

    /**
     * 将任意对象存入redis并设置逻辑过期时间
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void setWithLogicExpire(String key,Object value,Long time,TimeUnit unit){
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
    }
}
